(function( jQuery ){

    /**
     * Spring Layout Algorithm for placing nodes in the Canvas
     */
    function Spring(options) {

        var springSettings = {
            iterations                 : 500,
            maxRepulsiveForceDistance  : 6,
            k                          : 2, // this value is usually calculated as sqrt(area/|V|)
            c                          : 0.01,
            maxVertexMovement          : 0.5,
            spreadConstant             : 20
        }

        if(options) {
            jQuery.extend(springSettings, options);
        }

        this.settings = springSettings;

        /**
         *
         */
        this.layout = function() {
            for (var i = 0; i < this.settings.iterations; i++) {
                this.layoutIteration();
            }
            this.spreadNodesPositions(this.settings.spreadConstant);
        }

        this.spreadNodesPositions = function(spreadConstant) {
            for (var i in this.settings.nodes) {
                var node = this.settings.nodes[i];
                node.layoutPosX = node.layoutPosX * spreadConstant;
                node.layoutPosY = node.layoutPosY * spreadConstant;
            }
        }

        /**
         *
         */
        this.layoutIteration = function() {
            // Forces on nodes due to node-node repulsions
            for (var i = 0; i < this.settings.nodes.length; i++) {
                var node1 = this.settings.nodes[i];
                for (var j = i + 1; j < this.settings.nodes.length; j++) {
                    var node2 = this.settings.nodes[j];
                    this.layoutRepulsive(node1, node2);
                }
            }
            // Forces on nodes due to edge attractions
            for (i = 0; i < this.settings.edges.length; i++) {
                var edge = this.settings.edges[i];
                this.layoutAttractive(edge);
            }

            // Move by the given force
            for (i in this.settings.nodes) {
                var node = this.settings.nodes[i];
                var xmove = this.settings.c * node.layoutForceX;
                var ymove = this.settings.c * node.layoutForceY;

                var max = this.settings.maxVertexMovement;
                if(xmove > max) xmove = max;
                if(xmove < -max) xmove = -max;
                if(ymove > max) ymove = max;
                if(ymove < -max) ymove = -max;

                node.layoutPosX += xmove;
                node.layoutPosY += ymove;
                node.layoutForceX = 0;
                node.layoutForceY = 0;
            }
        }

        /**
         *
         */
        this.layoutRepulsive = function(node1, node2) {
            var dx = node2.layoutPosX - node1.layoutPosX;
            var dy = node2.layoutPosY - node1.layoutPosY;
            var d2 = dx * dx + dy * dy;
            if(d2 < 0.01) {
                dx = 0.1 * Math.random() + 0.1;
                dy = 0.1 * Math.random() + 0.1;
                d2 = dx * dx + dy * dy;
            }
            var d = Math.sqrt(d2);
            if(d < this.settings.maxRepulsiveForceDistance) {
                var repulsiveForce = this.settings.k * this.settings.k / d;
                node2.layoutForceX += repulsiveForce * dx / d;
                node2.layoutForceY += repulsiveForce * dy / d;
                node1.layoutForceX -= repulsiveForce * dx / d;
                node1.layoutForceY -= repulsiveForce * dy / d;
            }
        }

        /**
         *
         */
        this.layoutAttractive = function(edge) {
            var node1 = edge.source;
            var node2 = edge.target;

            var dx = node2.layoutPosX - node1.layoutPosX;
            var dy = node2.layoutPosY - node1.layoutPosY;
            var d2 = dx * dx + dy * dy;
            if(d2 < 0.01) {
                dx = 0.1 * Math.random() + 0.1;
                dy = 0.1 * Math.random() + 0.1;
                d2 = dx * dx + dy * dy;
            }
            var d = Math.sqrt(d2);
            if(d > this.settings.maxRepulsiveForceDistance) {
                d = this.settings.maxRepulsiveForceDistance;
                d2 = d * d;
            }
            var attractiveForce = (d2 - this.settings.k * this.settings.k) / this.settings.k;
            if(edge.attraction == undefined) edge.attraction = 1;
            attractiveForce *= Math.log(edge.attraction) * 0.5 + 1;

            node2.layoutForceX -= attractiveForce * dx / d;
            node2.layoutForceY -= attractiveForce * dy / d;
            node1.layoutForceX += attractiveForce * dx / d;
            node1.layoutForceY += attractiveForce * dy / d;
        }
    }

    /**
     * Purpose of this object is to manage mouse events in the canvas
     */
    function Mouse(options) {

        var mouseSettings = {};
        if(options) {
            jQuery.extend(mouseSettings, options);
        }

        this.settings = mouseSettings;

        /**
         * Gets current mouse position in the jQuery object
         * @param event Mouse Event
         * @param element jQuery object on which we are currently pointing
         * @return Mouse position in the canvas
         */
        this.getMouse = function(event, element) {
            var position = element.offset();
            var mouse = {
                x : Math.floor(event.pageX - position.left),
                y : Math.floor(event.pageY - position.top)
            }
            return mouse;
        }

        /**
         * Gets the current pointed node or null if there is no node with the pointed cursor
         * @param mouse Current mouse position
         * @return Current pointed node or null otherwise
         */
        this.getNodeWithCursor = function(mouse) {
            for(var i in this.settings.nodes) {
                var node = this.settings.nodes[i];
                var difX = (node.layoutPosX + this.settings.width/2) - mouse.x;
                var difY = (node.layoutPosY + this.settings.height/2) - mouse.y;
                var hypotenuse = Math.sqrt(Math.pow(difX, 2) + Math.pow(difY, 2));
                if(hypotenuse <= 15) {
                    return node;
                }
            }
            return null;
        }

        /**
         * Gets the current pointed edge or null if there is no edge with the pointed curor
         * @param mouse Current mouse position
         * @return Current pointed edge or null otherwise
         */
        this.getEdgeWithCursor = function(mouse) {
            for(var i in this.settings.edges) {
                var edge = {
                    source : {
                        layoutPosX : 0,
                        layoutPosY : 0
                    },
                    target : {
                        layoutPosX : 0,
                        layoutPosY : 0
                    }
                };
                //jQuery.extend(edge, this.settings.edges[i]);
                edge.source.layoutPosX = this.settings.edges[i].source.layoutPosX + this.settings.width/2;
                edge.target.layoutPosX = this.settings.edges[i].target.layoutPosX + this.settings.width/2;
                edge.source.layoutPosY = this.settings.edges[i].source.layoutPosY + this.settings.height/2;
                edge.target.layoutPosY = this.settings.edges[i].target.layoutPosY + this.settings.height/2;
                var vector = {
                    u : edge.target.layoutPosX - edge.source.layoutPosX,
                    v : edge.target.layoutPosY - edge.source.layoutPosY
                }
                var normalVector = {
                    u : edge.target.layoutPosY - edge.source.layoutPosY,
                    v : -(edge.target.layoutPosX - edge.source.layoutPosX)
                }
                /*
                 * a2 : normalVector.u*x + normalVector.v * y + c1 = 0
                 * a1 : vector.u*x + vector.v*y + c2 = 0
                 */
                var c1 = -(normalVector.u * edge.source.layoutPosX + normalVector.v * edge.source.layoutPosY);
                var c2 = -(vector.u * mouse.x + vector.v * mouse.y);
                var intersection = {}
                intersection.y = ((vector.u * c1) - (normalVector.u * c2))/(-(vector.u * normalVector.v) + (normalVector.u * vector.v));
                intersection.x = -((normalVector.v * intersection.y) + c1) / normalVector.u;
                /*
                 *  x postion is between begining and end
                 */
                if((edge.source.layoutPosX > intersection.x && intersection.x > edge.target.layoutPosX) || (edge.source.layoutPosX < intersection.x && intersection.x < edge.target.layoutPosX)) {
                    /*
                     * y postion is between beginning and end
                     */
                    if((edge.source.layoutPosY > intersection.y && intersection.y > edge.target.layoutPosY) || (edge.source.layoutPosY < intersection.y && intersection.y < edge.target.layoutPosY)) {
                        var vector1 = {
                            u : intersection.x - mouse.x,
                            v : intersection.y - mouse.y
                        }
                        var vectorSize = Math.sqrt(Math.pow(vector1.u,2) + Math.pow(vector1.v, 2));
                        if (vectorSize <= 5) {
                            return this.settings.edges[i];
                        }
                    }
                }
            }
            return null;
        }
    }

    var settings = null;
    var context = null;
    var mouse = null;

    var methods = {
        /**
         * Initialize the plugin
         */
        init : function(options) {
            return jQuery(this).each(function(){

                settings = {
                    nodes : {},
                    edges : {},
                    width : 200,
                    height : 200,
                    background : "#FFFFFF",
                    border : "1px #AFAFAF solid"
                }

                if (options) {
                    jQuery.extend(settings, options);
                }

                var id = jQuery(this).attr("id");
                var jQueryObject = jQuery(this).html("<canvas id='gCanvas" + id + "' width='" + settings.width + "' height='" + settings.height + "'></canvas>");

                jQuery("#gCanvas" + id)
                .css("background", settings.background)
                .css("border", settings.border);

                // initialize canvas object
                var canvas = document.getElementById("gCanvas" + id);

                if(!canvas) {
                    jQuery.error("Unable to locate canvas object in the document.");
                    return;
                }
                if(!canvas.getContext) {
                    jQuery.error("There is no context for the provided canvas.");
                    return;
                }
                context = canvas.getContext('2d');
                //change center of the canvas [0,0] is in the middle of the canvas
                context.translate(settings.width/2, settings.height/2);
                //set all the nodes values to 0
                jQuery.each(settings.nodes, function(v, node) {
                    var node1 = {
                        layoutPosX          : 0,
                        layoutPosY          : 0,
                        layoutForceX        : null,
                        layoutForceY        : null,
                        fillStyle           : 'white',
                        strokeStyle         : 'black',
                        textColor           : 'black',
                        hoverFillStyle      : 'white',
                        hoverStrokeStyle    : 'red',
                        hoverTextColor      : 'red'
                    }
                    jQuery.extend(node1, node);
                    jQuery.extend(node, node1);
                });

                jQuery.each(settings.edges, function(v, edge) {
                    var edge1 = {
                        fillStyle           : 'black',
                        strokeStyle         : 'black',
                        textColor           : 'black',
                        hoverFillStyle      : 'white',
                        hoverStrokeStyle    : 'red',
                        hoverTextColor      : 'red'
                    }
                    jQuery.extend(edge1, edge);
                    jQuery.extend(edge, edge1);
                });

                jQueryObject.data("graph", {
                    settings : settings,
                    context : context
                });

                var layout = true;
                jQuery.each(settings.nodes, function(v, node) {
                    if (node.layoutPosX != 0 || node.layoutPosY != 0) {
                        layout = false;
                    }
                });

                if(layout) {
                    methods.layout();
                }

                mouse = new Mouse(settings);

                jQueryObject.mouseenter(function() {
                    settings = {}
                    jQuery.extend(settings, jQuery(this).data('graph').settings);
                    context = jQuery(this).data('graph').context;
                    mouse = new Mouse(settings);
                });

                /**
                 * Mouse click in the Canvas
                 * @event MouseClick
                 */
                jQueryObject.click(function(event) {
                    /**
                     * Clicked node
                     */
                    var node = mouse.getNodeWithCursor(mouse.getMouse(event, jQueryObject));
                    if(node != null) {
                        if(jQuery.isFunction(node.onClick)) {
                            node.onClick();
                        }
                    }

                    /**
                     * Clicked edge
                     */
                    var edge = mouse.getEdgeWithCursor(mouse.getMouse(event, jQueryObject));
                    if(edge != null) {
                        if(jQuery.isFunction(edge.onClick)) {
                            edge.onClick();
                        }
                    }
                });

                /**
                 * Mouse doubleclick in the Canvas
                 * @event MouseDoubleClick
                 */
                jQueryObject.dblclick(function(event) {
                    /**
                     * Clicked node
                     */
                    var node = mouse.getNodeWithCursor(mouse.getMouse(event, jQueryObject));
                    if(node != null) {
                        if(jQuery.isFunction(node.onDblClick)) {
                            node.onDblClick();
                        }
                    }

                    /**
                     * Clicked edge
                     */
                    var edge = mouse.getEdgeWithCursor(mouse.getMouse(event, jQueryObject));
                    if(edge != null) {
                        if(jQuery.isFunction(edge.onDblClick)) {
                            edge.onDblClick();
                        }
                    }
                });

                var oldNode = null;
                var oldEdge = null;
                var oldNodeFillStyle = null;
                var oldNodeStrokeStyle = null;
                var oldNodeTextColor = null;
                var oldEdgeFillStyle = null;
                var oldEdgeStrokeStyle = null;
                var oldEdgeTextColor = null;
                /**
                 * Mouse movement on the Canvas
                 * @event MouseOver
                 */
                jQueryObject.mousemove(function(event) {

                    var node = mouse.getNodeWithCursor(mouse.getMouse(event, jQueryObject));

                    var change = false;
                    if(node != null && node != oldNode && (jQuery.isFunction(node.onClick) || jQuery.isFunction(node.onDblClick))) {
                        oldNodeFillStyle = node.fillStyle;
                        oldNodeStrokeStyle = node.strokeStyle;
                        oldNodeTextColor = node.textColor;
                        oldNode = node;
                        node.fillStyle = node.hoverFillStyle;
                        node.strokeStyle = node.hoverStrokeStyle;
                        node.textColor = node.hoverTextColor;
                        change = true;
                    }

                    if(node == null && node != oldNode) {
                        oldNode.fillStyle = oldNodeFillStyle;
                        oldNode.strokeStyle = oldNodeStrokeStyle;
                        oldNode.textColor = oldNodeTextColor;
                        oldNode = null;
                        change = true;
                    }

                    var edge = mouse.getEdgeWithCursor(mouse.getMouse(event, jQueryObject));

                    if(edge != null && edge != oldEdge && (jQuery.isFunction(edge.onClick) || jQuery.isFunction(edge.onDblClick))) {
                        oldEdgeFillStyle = edge.fillStyle;
                        oldEdgeStrokeStyle = edge.strokeStyle;
                        oldEdgeTextColor = edge.textColor;
                        oldEdge = edge;
                        edge.fillStyle = edge.hoverFillStyle;
                        edge.strokeStyle = edge.hoverStrokeStyle;
                        edge.textColor = edge.hoverTextColor;
                        change = true;
                    }

                    if(edge == null && edge != oldEdge) {
                        oldEdge.fillStyle = oldEdgeFillStyle;
                        oldEdge.strokeStyle = oldEdgeStrokeStyle;
                        oldEdge.textColor = oldEdgeTextColor;
                        oldEdge = null;
                        change = true;
                    }

                    if(change) {
                        methods.clear();
                        methods.show();
                        change = false;
                    }

                    if((edge != null && (jQuery.isFunction(edge.onClick) || jQuery.isFunction(edge.onDblClick))) || (node != null && (jQuery.isFunction(node.onClick) || jQuery.isFunction(node.onDblClick)))) {
                        jQuery(this).css('cursor', 'pointer');
                    } else {
                        jQuery(this).css('cursor', 'default');
                    }
                });
            });
        },
        /**
         * This method will paint all the edges and nodes on the canvas
         * @return jQuery function type
         */
        show : function() {
            return jQuery(this).each(function(){
                jQuery.each(settings.nodes, function(v, node) {
                    methods.drawNode(node);
                });

                jQuery.each(settings.edges, function(v, edge) {
                    methods.drawEdge(edge);
                });
            })
        },
        /**
         * Draws the node on the canvas
         * @param node node to be painted
         */
        drawNode : function(node) {
            context.fillStyle = node.fillStyle;
            context.strokeStyle = node.strokeStyle;
            context.beginPath();
            context.arc(node.layoutPosX, node.layoutPosY, 15, 0, Math.PI*2, true);
            context.fill();
            context.stroke();
            context.closePath();
            if(node.toString() != "[object Object]" || node.value) {
                context.fillStyle = node.textColor;
                var length = node.value? node.value.length : node.toString().length;
                context.fillText(node.value || node.toString(), node.layoutPosX - (length * 5.5 / 2), node.layoutPosY+3);
            }

        },
        /**
         * Draws the edge on the canvas
         * @param edge edge to be painted
         */
        drawEdge : function(edge) {
            //initialize properties
            var vector = null;
            var vectorSize = 0;
            var normalVector = null;
            /**
                 * k is the ratio value for moving end of direction arrows and
                 * labels of the edge in the normal vector direction
                 */
            var k = 0;
            var ratio = 0;
            var difX = 0;
            var difY = 0;

            //edge vector
            vector = {
                u : edge.target.layoutPosX - edge.source.layoutPosX,
                v : edge.target.layoutPosY - edge.source.layoutPosY
            }
            //edge vector size
            vectorSize = Math.sqrt(Math.pow(vector.u, 2) + Math.pow(vector.v, 2));

            //draw edge on the canvas
            context.fillStyle = edge.fillStyle;
            context.strokeStyle = edge.strokeStyle;
            context.beginPath();
            context.moveTo(edge.source.layoutPosX, edge.source.layoutPosY);
            context.lineTo(edge.target.layoutPosX, edge.target.layoutPosY);
            context.stroke();
            context.closePath();

            // show additional data to the edge
            // edge value
            // edge direction
            if(edge.value || (edge.directed == true) || edge.toString() != "[object Object]") {
                //edge normal vector
                normalVector = {
                    u : edge.target.layoutPosY - edge.source.layoutPosY,
                    v : -(edge.target.layoutPosX - edge.source.layoutPosX)
                }
                // edge value
                if(edge.value || edge.toString() != "[object Object]") {
                    k = 5 / vectorSize;
                    //edge middle point
                    var midPoint = {
                        x : (edge.target.layoutPosX + edge.source.layoutPosX) / 2,
                        y : (edge.target.layoutPosY + edge.source.layoutPosY) / 2
                    }
                    context.fillStyle = edge.textColor;
                    context.fillText(edge.value || edge.toString(), midPoint.x + normalVector.u * k, midPoint.y + normalVector.v * k);
                }
                //edge direction
                if(edge.directed) {
                    k = 10 / vectorSize;
                    ratio = vectorSize / 10;
                    difX = Math.abs(edge.source.layoutPosX - edge.target.layoutPosX) / ratio;
                    difY = Math.abs(edge.source.layoutPosY - edge.target.layoutPosY) / ratio;

                    var x = edge.target.layoutPosX;
                    var y = edge.target.layoutPosY;

                    if(edge.target.layoutPosX > edge.source.layoutPosX) {
                        x -= difX;
                    } else {
                        x += difX;
                    }

                    if(edge.target.layoutPosY > edge.source.layoutPosY) {
                        y -= difY;
                    } else {
                        y += difY;
                    }

                    context.beginPath();
                    context.moveTo(edge.target.layoutPosX, edge.target.layoutPosY);
                    context.lineTo(x + normalVector.u * k, y + normalVector.v * k);
                    context.moveTo(edge.target.layoutPosX, edge.target.layoutPosY);
                    context.lineTo(x - normalVector.u * k, y - normalVector.v * k);
                    context.stroke();
                    context.closePath();

                }
            }
        },
        /**
         * This method will clear the canvas
         * @return jQuery function type
         */
        clear : function() {
            return jQuery(this).each(function(){
                context.clearRect(0 - settings.width/2, 0 - settings.height/2, settings.width, settings.height);
            });
        },
        /**
         * Calculate edges source and target position
         */
        recalculateEdges : function() {
            jQuery.each(settings.edges, function(v, edge) {
                //initialize properties
                var vector = {};
                var vectorSize = 0;
                var ratio = 0;
                var difX = 0;
                var difY = 0;

                //vector of the edge
                vector = {
                    u : edge.target.layoutPosX - edge.source.layoutPosX,
                    v : edge.target.layoutPosY - edge.source.layoutPosY
                }
                //size of the vector defined by the edge
                vectorSize = Math.sqrt(Math.pow(vector.u, 2) + Math.pow(vector.v, 2));
                //ratio between vector size and radius of the node circle
                ratio = vectorSize / 15;
                //dif defines begining and end postion of the edge
                difX = Math.abs(edge.source.layoutPosX - edge.target.layoutPosX) / ratio;
                difY = Math.abs(edge.source.layoutPosY - edge.target.layoutPosY) / ratio;

                var edgeSettings = {
                    source : {
                        layoutPosX : 0,
                        layoutPosY : 0
                    },
                    target : {
                        layoutPosX : 0,
                        layoutPosY : 0
                    }
                };
                //change edge begin and end position. Edge will be connected to the circle not to the center of the circle.
                if(edge.source.layoutPosX > edge.target.layoutPosX) {
                    edgeSettings.source.layoutPosX = edge.source.layoutPosX - difX;
                    edgeSettings.target.layoutPosX = edge.target.layoutPosX + difX;
                } else {
                    edgeSettings.source.layoutPosX = edge.source.layoutPosX + difX;
                    edgeSettings.target.layoutPosX = edge.target.layoutPosX - difX;
                }

                if(edge.source.layoutPosY > edge.target.layoutPosY) {
                    edgeSettings.source.layoutPosY = edge.source.layoutPosY - difY;
                    edgeSettings.target.layoutPosY = edge.target.layoutPosY + difY;
                } else {
                    edgeSettings.source.layoutPosY = edge.source.layoutPosY + difY;
                    edgeSettings.target.layoutPosY = edge.target.layoutPosY - difY;
                }

                jQuery.extend(edge, edgeSettings);
            });
        },
        /**
         * Layout the graph in the canvas
         * @return jQuery function type
         */
        layout : function() {
            return jQuery(this).each(function() {
                var spring = new Spring(settings);
                spring.layout();
                methods.recalculateEdges();
            });
        },
        /**
         * Gets the context of the current graph. Asigne settings and context
         * variables values
         * @TODO needs to add this feature
         */
        getContext : function() {
            return jQuery(this).each(function() {
                settings = {}
                jQuery.extend(settings, jQuery(this).data('graph').settings);
                context = jQuery(this).data('graph').context;
                mouse = new Mouse(settings);
            });
        }
    }

    jQuery.fn.graphs = function(method) {
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || ! method) {
            return methods.init.apply(this, arguments);
        } else {
            jQuery.error('Method ' +  method + ' does not exist on jQuery.graphs');
            return false;
        }
    };

})( jQuery );